<template>
  <div class="zip-demo">
    <h2>ZIP 文件解析 Demo</h2>
    
    <!-- 文件选择器 -->
    <div class="file-input-container">
      <input 
        type="file" 
        accept=".zip" 
        @change="handleFileSelect"
        ref="fileInput"
      />
      <button @click="triggerFileSelect">选择 ZIP 文件</button>
    </div>
    
    <!-- 解析状态 -->
    <div v-if="loading" class="loading">
      正在解析 ZIP 文件...
    </div>
    
    <!-- 错误信息 -->
    <div v-if="error" class="error">
      {{ error }}
    </div>
    
    <!-- 解析结果 -->
    <div v-if="zipStructure" class="result">
      <h3>ZIP 文件结构:</h3>
      <pre>{{ formatStructure(zipStructure) }}</pre>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import JSZip from 'jszip'

// 响应式数据
const fileInput = ref<HTMLInputElement | null>(null)
const loading = ref(false)
const error = ref('')
const zipStructure = ref<any>(null)

// 触发文件选择
const triggerFileSelect = () => {
  fileInput.value?.click()
}

// 处理文件选择
const handleFileSelect = async (event: Event) => {
  const input = event.target as HTMLInputElement
  const file = input.files?.[0]
  
  if (!file) {
    return
  }
  
  // 检查文件类型
  if (!file.name.endsWith('.zip')) {
    error.value = '请选择一个 ZIP 文件'
    return
  }
  
  // 重置状态
  error.value = ''
  loading.value = true
  zipStructure.value = null
  
  try {
    // 使用 JSZip 解析文件，启用 decodeFileName 选项处理中文
    const zip = new JSZip()
    const loadedZip = await zip.loadAsync(file, {
      decodeFileName: decodeFileName // 使用自定义解码函数
    })
    const res = await getFileTreeFromZip(file)
    console.log("res",res)
    // 构建文件结构树
    const structure = buildStructure(loadedZip)
    zipStructure.value = structure
  } catch (err) {
    error.value = `解析 ZIP 文件时出错: ${err.message}`
    console.error(err)
  } finally {
    loading.value = false
  }
}

// 自定义文件名解码函数
const decodeFileName = (bytes: Uint8Array | string) => {
  if (typeof bytes === 'string') {
    return bytes
  }
  
  // 尝试使用 UTF-8 解码
  try {
    return new TextDecoder('utf-8').decode(bytes)
  } catch (e) {
    // 如果 UTF-8 解码失败，回退到默认处理
    return Array.from(bytes).map(byte => String.fromCharCode(byte)).join('')
  }
}

// 构建文件结构树
const buildStructure = (zip: JSZip) => {
  const structure: any = {
    name: 'root',
    type: 'folder',
    children: []
  }
  
  // 遍历所有文件
  zip.forEach((relativePath, zipEntry) => {
    // 使用解码后的文件名
    const decodedPath = typeof zipEntry.name === 'string' ? zipEntry.name : decodeFileName(zipEntry.name)
    const pathParts = decodedPath.split('/').filter(part => part.length > 0)
    let current = structure.children
    
    pathParts.forEach((part, index) => {
      // 检查是否已存在同名节点
      let node = current.find((item: any) => item.name === part)
      
      if (!node) {
        node = {
          name: part,
          type: index === pathParts.length - 1 && !zipEntry.dir ? 'file' : 'folder',
          children: index === pathParts.length - 1 && !zipEntry.dir ? undefined : []
        }
        current.push(node)
      }
      
      // 如果是文件夹，继续深入
      if (index < pathParts.length - 1 || zipEntry.dir) {
        current = node.children
      }
    })
  })
  
  return structure
}

// 格式化显示结构
const formatStructure = (structure: any) => {
  const format = (items: any[], indent = 0) => {
    let result = ''
    const spaces = '  '.repeat(indent)
    
    items.forEach(item => {
      result += `${spaces}${item.type === 'folder' ? '📁' : '📄'} ${item.name}\n`
      if (item.children && item.children.length > 0) {
        result += format(item.children, indent + 1)
      }
    })
    
    return result
  }
  
  return format(structure.children)
}


// 定义树节点的数据结构
interface FileTreeNode {
  name: string;
  type: 'file' | 'directory';
  path: string;
  children?: FileTreeNode[];
}

// 定义树节点的数据结构
interface FileTreeNode {
  name: string;
  type: 'file' | 'directory';
  path: string;
  children?: FileTreeNode[];
}

/**
 * 读取压缩包文件并返回一个代表其文件层级的树形数据结构。
 * @param zipFile 压缩包文件 (File 或 Blob 对象).
 * @returns 返回一个 Promise，该 Promise resolve 为一个树形结构的数组。
 */
async function getFileTreeFromZip(zipFile: File | Blob): Promise<FileTreeNode[]> {
  const zip = await JSZip.loadAsync(zipFile);
  const root: { [key: string]: FileTreeNode } = {};

  // 使用 Object.entries() 来安全地遍历 zip 包中的所有文件
  // 这样可以避免 for...in 和 hasOwnProperty 的问题
  for (const [relativePath, zipObject] of Object.entries(zip.files)) {
    // 按 '/' 分割并移除因路径末尾的'/'而产生的空字符串
    const pathParts = relativePath.replace(/\/$/, '').split('/');

    // 如果路径为空（例如空的根目录条目），则跳过
    if (pathParts.length === 0 || (pathParts.length === 1 && !pathParts[0])) {
      continue;
    }

    let currentChildren = root;

    for (let i = 0; i < pathParts.length; i++) {
      const part = pathParts[i];
      const isFile = i === pathParts.length - 1 && !zipObject.dir;

      let node = currentChildren[part];

      // 如果节点不存在，则创建它
      if (!node) {
        node = {
          name: part,
          type: isFile ? 'file' : 'directory',
          path: pathParts.slice(0, i + 1).join('/'),
          // 如果是目录，则初始化 children 数组
          ...( !isFile && { children: [] } )
        };
        currentChildren[part] = node;
      }

      // 如果是目录，则进入下一层 children
      if (node.type === 'directory' && node.children) {
        // 我们将 children 数组临时转换为对象以便快速查找
        // 但这里我们简化一下，直接在对象上操作
        // 注意：这里需要确保 children 被正确处理为下一个 currentChildren
        // 为了实现这一点，我们需要将 children 存储为对象，然后在最后转换
      }
    }
  }

  // 为了更准确地构建树，我们需要稍微调整一下逻辑
  // 下面是一个更健壮的最终实现
  return buildTree(zip.files);
}

// 辅助函数，使逻辑更清晰
function buildTree(files: { [key: string]: JSZip.JSZipObject }): FileTreeNode[] {
    const tree: FileTreeNode[] = [];
    const map: { [key: string]: FileTreeNode } = {};

    for (const [fullPath, zipObject] of Object.entries(files)) {
        // 忽略空的或无效的路径
        if (!fullPath || fullPath.endsWith('__MACOSX/')) continue;

        const cleanPath = fullPath.replace(/\/$/, ''); // 移除末尾的斜杠
        const pathParts = cleanPath.split('/');

        pathParts.forEach((part, index) => {
            const currentPath = pathParts.slice(0, index + 1).join('/');
            if (map[currentPath]) return; // 如果已处理，则跳过

            const isFile = index === pathParts.length - 1 && !zipObject.dir;
            const node: FileTreeNode = {
                name: part,
                path: currentPath,
                type: isFile ? 'file' : 'directory',
            };
            if (node.type === 'directory') {
                node.children = [];
            }

            map[currentPath] = node;

            if (index > 0) {
                const parentPath = pathParts.slice(0, index).join('/');
                const parentNode = map[parentPath];
                if (parentNode && parentNode.children) {
                    parentNode.children.push(node);
                }
            } else {
                tree.push(node);
            }
        });
    }

    return tree;
}
</script>

<style scoped>
.zip-demo {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.file-input-container {
  margin-bottom: 20px;
}

.file-input-container input[type="file"] {
  display: none;
}

.file-input-container button {
  padding: 10px 20px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
}

.file-input-container button:hover {
  background-color: #359c6d;
}

.loading {
  color: #007acc;
  font-style: italic;
}

.error {
  color: #e74c3c;
  background-color: #fdf2f2;
  padding: 10px;
  border-radius: 4px;
  margin: 10px 0;
}

.result {
  margin-top: 20px;
}

.result h3 {
  margin-bottom: 10px;
}

.result pre {
  background-color: #f5f5f5;
  padding: 15px;
  border-radius: 4px;
  overflow-x: auto;
  white-space: pre-wrap;
  word-wrap: break-word;
}
</style>